import logging

from flask import Response
from memoization import cached

from lakesuperior import env as lsenv
from lakesuperior.api import query as qry_api
from lakesuperior.api import resource as rsrc_api
from lakesuperior.dictionaries.namespaces import ns_collection as nsc
from rdflib import Literal

from grayspread import store
from grayspread.config import config
from grayspread.store import txn_ctx

logger = logging.getLogger(__name__)


def uri_to_uid(uri):
    """ Convert a lsup URI to an internal UID. """
    return uri.replace(nsc['fcres'], '')


# # # Blueprint helpers # # #

def fmt_dimensions(rsrc):
    """
    Format a resource dimensions.

    Return:
        list: A list of tuples containig:

        - Literal rendition of the dimensions
        - Dictionary of numeric values to plug into drawing functions.

        The list is sorted alphabetically by the literal description.
    """
    dim_nodes = rsrc.imr[rsrc.uri: nsc['lii']['measurement']]
    dims = []
    for dnode in dim_nodes:
        dim_parts = []
        logger.info(f'Dim node: {dnode}')
        unit = rsrc.imr[dnode: nsc['lii']['unit']].pop()
        labels = rsrc.imr[dnode: nsc['skos']['prefLabel']]

        try:
            di = rsrc.imr[dnode: nsc['lii']['diameter']].pop()
        except KeyError:
            di = 0
        try:
            w = rsrc.imr[dnode: nsc['lii']['width']].pop()
        except KeyError:
            w = 0
        try:
            h = rsrc.imr[dnode: nsc['lii']['height']].pop()
        except KeyError:
            h = 0
        try:
            d = rsrc.imr[dnode: nsc['lii']['depth']].pop()
        except KeyError:
            d = 0

        dim_values = {
                'w': float(w), 'h': float(h), 'd': float(d), 'di': float(di)}

        if di:
            dim_parts.append(f'Ø: {di} {unit}')
        if w:
            dim_parts.append(f'width: {w} {unit}')
        if h:
            dim_parts.append(f'height: {h} {unit}')
        if d:
            dim_parts.append(f'depth: {d} {unit}')

        for label in labels:  # There should be one, but you never know...
            dims.append((
                (f'{label}: ' + '; '.join(dim_parts).capitalize()),
                dim_values
            ))

    return sorted(dims, key=lambda x: x[0])


def inv_rel(rsrc, p):
    """
    Set of resources pointing to this resource based on predicate.
    """
    return rsrc.imr[:p:rsrc.uri]


def get_values(rsrc, pred):
    """ Return a set of values for a resource predicate. """
    return {str(o) for o in rsrc.imr[rsrc.uri: pred]}


def get_value(rsrc, pred):
    """ Return a single value for a resource predicate. """
    ret = rsrc.imr.value(pred)

    return str(ret) if ret else ''


@cached(ttl=60)
def link_meta(uri):
    """
    Get basic metadata for a resource to be used to build a hyperlink.

    Return:
        tuple: uid and preferred label.
    """
    rsrc = rsrc_api.get(uri_to_uid(uri))
    if get_lii_type(rsrc) == nsc['lii']['Document']:
        return (
            rsrc.uid,
            get_value(rsrc, nsc['skos'].prefLabel),
            rsrc.uid,
        )
    else:
        return (
            rsrc.uid,
            get_value(rsrc, nsc['skos'].prefLabel),
            get_value(rsrc, nsc['lii'].hasPreferredRepresentation).replace(
                    nsc['fcres'], ''),
        )


@cached(ttl=60)
def type_meta(type_id):
    """
    Get metadata about a lii type (class) from a resource stored in LS.

    Args:
        type_id (str): Type ID i.e. class name, e.g. ``Artifact`` or ``Place``.
    """
    uris = qry_api.triple_match(p=nsc['owl'].sameAs, o=nsc['lii'][type_id])

    return rsrc_api.get(uri_to_uid(uris.pop()))


def get_lii_type(rsrc):
    """
    Get main LII type for a resource.

    There should be only one.

    :rtype: URIRef
    """
    main_types = {nsc['lii'][t] for t in store.highlight_types}
    lii_types = store.get_lii_types(rsrc) & main_types

    if len(lii_types):
        # There should be only one!
        return lii_types.pop()


@cached(ttl=60)
def get_related_media(rsrc):
    """
    Get related media as set of ordered aggregation resources.
    """
    media = []
    for uri in get_values(rsrc, nsc['lii'].hasRelatedMedium):
        uid = uri_to_uid(uri)
        media.append((rsrc_api.get(uid), tuple(aggregation_list(uid))))

    from pprint import pformat
    logger.info(f'media: {pformat(media)}')

    return media


@cached(ttl=60)
def uid_from_local_uid(local_uid):
    """
    Get a resource by its local UID,
    """
    uri = qry_api.triple_match(
            p=nsc['lii'].localUid, o=Literal(local_uid)).pop()

    return uri.replace(nsc['fcres'], '')


def aggregation_list(uid):
    """
    Build an ordered list of resources from an aggregation UID.
    """
    agg = rsrc_api.get(uid)
    ars = []
    px_uri = agg.imr.value(nsc['nav']['first'])
    while px_uri:
        ar_match = qry_api.triple_match(
                s=px_uri, p=nsc['ore'].proxyFor, return_full=True)
        ars.append(link_meta(ar_match.pop()[2]))

        px_match = qry_api.triple_match(
                s=px_uri, p=nsc['nav']['next'], return_full=True)
        px_uri = px_match.pop()[2] if px_match else None

    return ars


def local_path(uid):
    """ Get the local file path of a LDP-NR relative to the root path. """
    rsrc = rsrc_api.get(uid)
    with txn_ctx():
        path = rsrc.local_path
    return path.replace(lsenv.app_globals.nonrdfly.root, '')


def image_iiif_url(uid, params):
    """ Get full IIIF URL from a public image UID. """
    local_uid = local_path(uid)
    url = f'{config["iiif_image_root_url"]}{local_uid}/{params}'
    logger.info(f"IIIF URL: {url}")

    return url


def stream(data, mimetype='application/octet-stream', headers={}):
    """ Stream out data from an incoming streaming request. """
    def _stream_contents(rsp):
        for chunk in rsp.iter_content(config["chunk_size"]):
            yield chunk

    return Response(_stream_contents(data), mimetype=mimetype, headers=headers)
